#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
发票OCR处理工具 - 基于通义千问多模态API

这个脚本专门用于处理发票图像的OCR识别，并保证数字保持原始格式。
"""

import os
import sys
import json
import argparse
from pathlib import Path
from rich.console import Console
from rich.markdown import Markdown
import base64

# 导入通义千问多模态工具
try:
    from qwen_multimodal import query_model, encode_image_to_base64
except ImportError:
    script_dir = os.path.dirname(os.path.abspath(__file__))
    sys.path.append(script_dir)
    from qwen_multimodal import query_model, encode_image_to_base64

console = Console()

# 添加debug模式标志
debug_mode = False

# 构建提示词
def build_prompt(preserve_numbers=True):
    """构建用于发票识别的提示词"""
    prompt = """提取发票内容为JSON格式。严格最高优先级要求:
1. 所有数字必须严格保持原始格式，不进行任何小数位数的标准化或舍入
2. 绝对禁止对任何数字进行四舍五入或取整，特别是单价字段
3. 如发票显示数字是7075.47169811321，则必须保持为"7075.47169811321"而不是"7075.47"或"7075.5"
4. 如发票显示数字是8.849558，则必须保持为"8.849558"，不得舍入为"8.85"
5. 如发票显示数字是2641.79，则必须保持为"2641.79"，不要猜测或添加更多小数位
6. 对于没有小数点的整数，不要添加小数位，保持原样
7. 严格按照发票上显示的每一个数字的每一位数字进行提取，完全保持原样
8. 不要包含非发票上显示的内容
9. 不要编造或猜测任何在发票上未明确看到的信息
10. 对于商品名称等文本内容，保持原始文本，不要简化或推测
11. 如果某个字段不确定、看不清或包含无法识别的字符，必须使用"-"符号表示，不要瞎编
12. 所有数值字段都必须以字符串形式返回，确保不丢失精度或格式
13. 严格只提取可以明确识别的汉字、字母和数字，对于无法识别的特殊符号或模糊文字，使用"-"代替
14. 细致识别所有重要字段，特别注意：
   - 收款人：发票上标明的收款人姓名，通常在底部区域
   - 复核：负责复核的人员姓名，通常在底部区域
   - 开票人：开具发票的人员姓名，通常在底部区域
   - 销售方印章：销售方盖章信息，通常是一个红色印章
   - 备注：发票上的备注内容区域
   - 购买方地址电话：完整提取地址和电话信息，通常格式为"地址 电话"
   - 购买方开户行及账号：完整提取银行和账号信息，通常格式为"XX银行 账号XXXXX"
   - 销售方地址电话：完整提取地址和电话信息
   - 销售方开户行及账号：完整提取银行和账号信息
   - 请识别发票中出现的“建筑服务”、“旅客运输服务”、“不动产经营租赁服务”、“成品油”和“货物服务”等字样，写到特定业务类型字段，若不存在这些字样该字段则为空。
15. 对于商品名称与规格型号：
   - 商品名称字段应只包含商品名称本身，例如"碳结团"而非"碳结圆"
   - 规格型号字段应只包含规格型号信息，例如"Φ90"
   - 如发票中显示为"碳结团Φ90"，应分离为"商品名称":"碳结团", "规格型号":"Φ90"
   - 商品名称中只识别明确可见的汉字、字母和数字，如包含无法识别的字符，用"-"代替
16. 对于地址电话和银行账号，要保持完整性：
   - 如果格式为"XX路XX号 电话:XXX"，完整提取到购买方地址电话或销售方地址电话
   - 如果格式为"XX银行XX支行 账号:XXXXX"，完整提取到购买方开户行及账号或销售方开户行及账号
17. 必须使用中文字段名，所有JSON字段名必须是中文，例如：
   - 发票类型（仅返回以下结果之一：普通发票、专用发票、统一发票）
   - 发票代码
   - 发票号码
   - 开票日期
   - 总金额（发票级别的合计金额，不含税）
   - 总税额（发票级别的合计税额）
   - 总价税合计大写（发票级别的合计金额，含税，数值上=总金额+总税额）
   - 总价税合计小写（发票级别的合计金额，含税，数值上=总金额+总税额）
   - 购买方名称
   - 购买方纳税人识别号
   - 购买方地址电话
   - 购买方开户行及账号
   - 销售方名称
   - 销售方纳税人识别号
   - 销售方地址电话
   - 销售方开户行及账号
   - 项目_货物_劳务_服务列表 (包含项目_货物_劳务_服务_名称, 项目_货物_劳务_服务_规格型号, 项目_货物_劳务_服务_单位, 
   项目_货物_劳务_服务_数量, 项目_货物_劳务_服务_单价, 项目_货物_劳务_服务_金额, 项目_货物_劳务_服务_税率, 项目_货物_劳务_服务_税额)
   - 特定业务类型
   - 机器号
18. 针对税务发票的特殊区域识别：
   - 发票右上角的开票日期，格式通常为"XXXX年XX月XX日"
   - 左侧的购买方和销售方信息区域
   - 中间的商品明细表格
   - 底部的金额合计区域
   - 最底部的收款人、复核人、开票人区域
19. 特别注意单价和金额字段的小数点：
   - 保留数字原始格式，包括所有小数位，如8.849558保持为8.849558
   - 如果发票上的数字是连续的如1752，应该识别为17.52
   - 所有数字要保留原始格式，不做四舍五入或位数截断
20. 严格处理模糊或不清晰的识别：
   - 对于无法确定的字符，一定要用"-"表示，而不是尝试猜测或将其替换为相似的字符
   - 对于模糊不清的商品名称，只保留能确定的部分，对不能确定的部分用"-"代替
   - 绝对禁止使用乱码或不符合中文、英文、数字规范的字符
   - 商品名称中不要出现"拉完"、"视亲"、"机算"之类的猜测名称，如果无法确定则使用"-"
21. 货物和服务项目识别特别要求：
   - 必须识别发票上的所有货物或服务项目，不遗漏任何一项
   - 从上到下处理所有商品项目，直到没有更多商品项
   - 某些发票可能有很多行商品，确保完整扫描整个商品表格区域
   - 对于多页发票，确保提取每一页上的所有商品
   - 不要依赖于预期项目数量，而是确保识别所有可见的商品项
22. 关于金额和税额字段的特别说明：
   - 在发票总体部分，使用'总金额'表示不含税的发票合计金额，使用'总税额'表示发票合计税额
   - 在商品列表中，每个商品项使用'项目_金额'表示该商品的不含税金额，使用'项目_税额'表示该商品的税额
   - 所有与商品/项目相关的字段名都以'项目_'开头，如项目_名称、项目_单位等"""
    
    return prompt

# 保持数字格式
def format_decimal(value):
    """格式化数字，完全保持原始格式和精度
    
    Args:
        value: 要格式化的数值（字符串或数字）
    
    Returns:
        str: 格式化后的字符串，保持原始格式
    """
    # 如果已经是字符串，直接返回
    if isinstance(value, str):
        return value
    
    # 将数字转换为字符串，保持原始格式
    if isinstance(value, (int, float)):
        # 使用str()直接转换，保持Python默认的字符串表示
        return str(value)
    
    return str(value)

def preserve_number_formats(result):
    """确保数字保持原始格式，不进行任何标准化或舍入"""
    if not isinstance(result, dict):
        return result
    
    # 递归处理嵌套字典
    for key, value in result.items():
        if isinstance(value, dict):
            result[key] = preserve_number_formats(value)
        elif isinstance(value, list):
            result[key] = [preserve_number_formats(item) if isinstance(item, dict) else format_decimal(item) for item in value]
        elif isinstance(value, (int, float, str)):
            # 所有数值都转换为字符串格式
            result[key] = format_decimal(value)
    
    return result

# 调用API
def call_api(prompt, image_path_or_base64):
    """调用模型API进行图像识别，增强错误处理和备选方案"""
    try:
        # 检查图像路径是否有效
        if isinstance(image_path_or_base64, str) and not image_path_or_base64.startswith("data:image"):
            # 检查文件是否超过大小限制
            try:
                file_size = os.path.getsize(image_path_or_base64) / (1024 * 1024)  # 转换为MB
                if file_size > 10:  # 如果图像超过10MB
                    console.print(f"[yellow]警告: 图像大小为 {file_size:.2f}MB，超过10MB可能导致API失败[/yellow]")
                    # 尝试压缩图像
                    try:
                        from PIL import Image
                        compressed_path = compress_image(image_path_or_base64)
                        if compressed_path:
                            console.print(f"[green]已将图像压缩并保存至: {compressed_path}[/green]")
                            image_path_or_base64 = compressed_path
                    except ImportError:
                        console.print("[yellow]缺少PIL库，无法压缩图像[/yellow]")
            except Exception as e:
                console.print(f"[yellow]检查图像大小时出错: {e}[/yellow]")

            # 尝试使用直接API调用方式
            try:
                try:
                    from qwen_multimodal import direct_api_call
                    console.print("[yellow]尝试使用直接API调用...[/yellow]")
                    response = direct_api_call(prompt, image_path_or_base64, preserve_numbers=True)
                    if response:
                        return response
                except Exception as e:
                    console.print(f"[yellow]直接API调用失败: {e}，尝试使用query_model...[/yellow]")
                
                # 尝试使用query_model
                try:
                    response = query_model(prompt, image_path_or_base64, preserve_numbers=True)
                    if response:
                        return response
                except Exception as e:
                    console.print(f"[yellow]query_model调用失败: {e}[/yellow]")
                
                # 如果两种方法都失败，尝试使用base64编码
                console.print("[yellow]常规API调用方法均失败，尝试使用base64编码...[/yellow]")
                try:
                    base64_image = encode_image_to_base64(image_path_or_base64)
                    from qwen_multimodal import direct_api_call
                    response = direct_api_call(prompt, base64_image, preserve_numbers=True)
                    return response
                except Exception as e:
                    console.print(f"[yellow]base64编码方法也失败: {e}[/yellow]")
                    raise
            except Exception as e:
                console.print(f"[red]所有API调用方法均失败: {e}[/red]")
                raise
        else:
            # 如果已经是base64字符串，直接尝试调用
            try:
                from qwen_multimodal import direct_api_call
                response = direct_api_call(prompt, image_path_or_base64, preserve_numbers=True)
                return response
            except Exception as e:
                console.print(f"[yellow]直接API调用失败: {e}[/yellow]")
                try:
                    response = query_model(prompt, image_path_or_base64, preserve_numbers=True)
                    return response
                except Exception as e:
                    console.print(f"[yellow]query_model调用也失败: {e}[/yellow]")
                    raise
        
        # 如果代码执行到这里，表示所有调用方法都失败了
        raise Exception("所有API调用方法都失败")
    except Exception as e:
        print(f"API调用失败: {e}")
        if debug_mode:
            import traceback
            traceback.print_exc()
        return None

def compress_image(image_path, quality=85, max_size=8.0):
    """压缩图像以减小文件大小
    
    Args:
        image_path: 原始图像路径
        quality: 压缩质量，1-100
        max_size: 压缩后的最大大小(MB)
        
    Returns:
        str: 压缩后的图像路径，如果压缩失败则返回None
    """
    try:
        from PIL import Image
        import os
        
        # 生成压缩图像的文件名
        path, ext = os.path.splitext(image_path)
        compressed_path = f"{path}_compressed{ext}"
        
        # 打开图像
        img = Image.open(image_path)
        
        # 保存压缩版本
        img.save(compressed_path, quality=quality, optimize=True)
        
        # 检查压缩后的大小
        compressed_size = os.path.getsize(compressed_path) / (1024 * 1024)  # 转换为MB
        
        # 如果仍然太大且图像较大，可以调整尺寸
        if compressed_size > max_size and (img.width > 1000 or img.height > 1000):
            # 计算缩放因子，尝试达到目标大小
            scale_factor = 0.8  # 初始缩放因子
            
            while compressed_size > max_size and scale_factor > 0.3:  # 限制最小缩放到原来的30%
                # 计算新尺寸
                new_width = int(img.width * scale_factor)
                new_height = int(img.height * scale_factor)
                
                # 调整图像大小
                resized_img = img.resize((new_width, new_height), Image.LANCZOS)
                
                # 保存调整大小后的图像
                resized_img.save(compressed_path, quality=quality, optimize=True)
                
                # 检查新文件大小
                compressed_size = os.path.getsize(compressed_path) / (1024 * 1024)
                
                # 减小缩放因子，为下一次迭代准备
                scale_factor *= 0.9
        
        return compressed_path
    except Exception as e:
        console.print(f"[red]压缩图像失败: {e}[/red]")
        return None

# 从响应中提取JSON
def extract_json_from_response(response):
    """从API响应中提取JSON内容"""
    if not response:
        return None
    
    try:
        # 先尝试简单的提取方式
        json_start = response.find("{")
        json_end = response.rfind("}")
        
        if json_start != -1 and json_end != -1:
            json_text = response[json_start:json_end+1]
            try:
                result = json.loads(json_text)
                return result
            except json.JSONDecodeError as e:
                print(f"简单提取后解析JSON失败: {e}")
                # 尝试修复常见的JSON结构问题
                json_text = fix_json_structure(json_text)
                try:
                    result = json.loads(json_text)
                    return result
                except:
                    pass  # 如果修复后仍无法解析，继续尝试下一种方法
        
        # 如果简单提取失败，尝试使用正则表达式提取完整JSON
        import re
        json_pattern = r'\{(?:[^{}]|(?:\{[^{}]*\}))*\}'
        matches = re.findall(json_pattern, response)
        
        if matches:
            # 尝试解析找到的最长匹配项
            matches.sort(key=len, reverse=True)
            for match in matches:
                try:
                    result = json.loads(match)
                    return result
                except:
                    continue
        
        # 所有提取方法都失败
        print("无法从响应中提取有效的JSON")
        return None
    except Exception as e:
        print(f"提取JSON过程中出错: {e}")
        if debug_mode:
            import traceback
            traceback.print_exc()
        return None

def fix_json_structure(json_text):
    """尝试修复常见的JSON结构问题"""
    import re
    
    # 1. 修复商品列表中的不完整项
    # 找到可能的商品列表结束位置缺失
    item_pattern = r'(\{\s*"商品名称":[^}]+?)(?=\s*\},|\s*\])'
    items = re.findall(item_pattern, json_text)
    
    # 如果找到不完整的项目，添加缺失的结束括号
    for item in items:
        if "}" not in item[-5:]:  # 如果项目末尾没有结束括号
            fixed_item = item + "}"
            json_text = json_text.replace(item, fixed_item)
    
    # 2. 修复商品列表数组的结束括号缺失
    list_pattern = r'("商品列表":\s*\[.*?)(?=\s*,\s*")'
    list_matches = re.findall(list_pattern, json_text)
    for list_match in list_matches:
        if "]" not in list_match[-5:]:  # 如果列表末尾没有结束括号
            fixed_list = list_match + "]"
            json_text = json_text.replace(list_match, fixed_list)
    
    # 3. 确保整个JSON对象有正确的结束括号
    if not json_text.strip().endswith("}"):
        json_text = json_text.rstrip() + "}"
    
    return json_text

# 修正数字和小数点格式
def fix_number_formats(result):
    """修正数字字段的格式，仅处理缺少小数点的情况"""
    if not isinstance(result, dict):
        return result
    
    # 递归处理嵌套字典
    for key, value in result.items():
        if isinstance(value, dict):
            result[key] = fix_number_formats(value)
        elif isinstance(value, list):
            result[key] = [fix_number_formats(item) if isinstance(item, dict) else value for item in value]
        elif isinstance(value, str) and key in ['单价', '金额', '税额', '总金额', '总税额',
                                              '项目_单价', '项目_金额', '项目_税额']:
            # 仅处理明显缺少小数点的情况
            if value.isdigit() and len(value) > 2:
                # 仅当确定是金额字段且明显缺少小数点时添加
                result[key] = value[:-2] + '.' + value[-2:]
    
    return result

def clean_text_content(text):
    """清理文本内容，处理无法识别的字符和猜测的内容"""
    if not isinstance(text, str):
        return text
    
    # 明显是乱编的内容
    suspicious_words = [
        '拉完', '视亲', '机算', '那个', '经细人', '都束花花', '还该', 
        '微信用户', '张三', '李四', '王五'
    ]
    
    # 检查是否包含可疑内容
    for word in suspicious_words:
        if word in text:
            return '-'  # 如果包含可疑内容，直接替换为破折号
    
    # 检查是否含有奇怪的符号组合，可能是乱码
    import re
    # 定义正常字符的模式，添加特殊符号支持
    normal_pattern = r'^[\u4e00-\u9fa5a-zA-Z0-9\s.,，。、:：;；()（）【】\[\]\/\\\-+%Φ°×±℃]+$'
    
    # 如果文本不符合正常字符模式，可能包含乱码
    if not re.match(normal_pattern, text):
        # 尝试提取所有有效的中文、英文、数字和特殊符号
        valid_chars = re.findall(r'[\u4e00-\u9fa5a-zA-Z0-9Φ°×±℃]+', text)
        if valid_chars:
            return ''.join(valid_chars)
        else:
            return '-'
    
    return text

def round_to_two_decimal_places(value):
    """Round a number to 2 decimal places and return as string"""
    try:
        if isinstance(value, str):
            value = value.replace('-', '0')
        return f"{float(value):.2f}"
    except (ValueError, TypeError):
        return "0.00"

def validate_and_fix_result(result):
    """验证结果的完整性并修复可能的问题"""
    if not isinstance(result, dict):
        return result
    
    # 1. 检查商品列表结构
    for item_key in ['商品列表', '货物或应税劳务、服务名称', '商品或服务清单', '项目列表', '服务列表']:
        if item_key in result and isinstance(result[item_key], list):
            # 检查每个商品项是否完整
            fixed_items = []
            for item in result[item_key]:
                if isinstance(item, dict):
                    # 重新映射键名，使用"项目_"前缀
                    new_item = {}
                    key_mapping = {
                        '商品名称': '项目_名称',
                        '名称': '项目_名称',
                        '货物或应税劳务、服务名称': '项目_名称',
                        '规格型号': '项目_规格型号',
                        '商品规格型号': '项目_规格型号',
                        '单位': '项目_单位',
                        '数量': '项目_数量',
                        '单价': '项目_单价',
                        '金额': '项目_金额',
                        '税率': '项目_税率',
                        '税额': '项目_税额'
                    }
                    
                    # 转换键名
                    for old_key, value in item.items():
                        new_key = key_mapping.get(old_key, old_key)
                        if not new_key.startswith('项目_'):
                            # 对于未明确映射的键，也添加前缀
                            new_key = f"项目_{old_key}"
                        new_item[new_key] = value
                    
                    # 确保必要字段存在
                    required_fields = ['项目_名称', '项目_单位', '项目_数量', '项目_单价', '项目_金额', '项目_税率', '项目_税额']
                    for field in required_fields:
                        if field not in new_item:
                            # 尝试查找可能有但没被映射的键
                            orig_field = field.replace('项目_', '')
                            if orig_field in item:
                                new_item[field] = item[orig_field]
                            else:
                                new_item[field] = '-'
                    
                    fixed_items.append(new_item)
            
            # 更新商品列表
            result[item_key] = fixed_items
    
    # 重命名列表键
    if '商品列表' in result:
        result['项目列表'] = result.pop('商品列表')
    elif '货物或应税劳务、服务名称' in result:
        result['项目列表'] = result.pop('货物或应税劳务、服务名称')
    elif '商品或服务清单' in result:
        result['项目列表'] = result.pop('商品或服务清单')
    
    # 2. 计算发票级总计字段 - 只有没有这些字段时才计算，避免覆盖已有的值
    if '项目列表' in result and isinstance(result['项目列表'], list) and len(result['项目列表']) > 0:
        try:
            # 计算商品列表中的商品金额总和 -> 总金额(合计金额)
            if '总金额' not in result:
                total_amount = sum(float(item.get('项目_金额', '0').replace('-', '0')) 
                               for item in result['项目列表'] 
                               if isinstance(item, dict))
                result['总金额'] = round_to_two_decimal_places(total_amount)
                
            # 计算商品列表中的商品税额总和 -> 总税额(合计税额)  
            if '总税额' not in result:
                total_tax = sum(float(item.get('项目_税额', '0').replace('-', '0')) 
                              for item in result['项目列表'] 
                              if isinstance(item, dict))
                result['总税额'] = round_to_two_decimal_places(total_tax)
                
        except Exception as e:
            if debug_mode:
                print(f"计算总计字段时出错: {e}")
    
    # 移除价税合计字段
    if '价税合计' in result:
        del result['价税合计']
                
    return result

def clean_result(result):
    """清理结果中可能的乱编内容"""
    if not isinstance(result, dict):
        return result
    
    # 递归处理嵌套字典
    for key, value in result.items():
        if isinstance(value, dict):
            result[key] = clean_result(value)
        elif isinstance(value, list):
            result[key] = [clean_result(item) if isinstance(item, dict) else 
                          (clean_text_content(item) if isinstance(item, str) else item) 
                          for item in value]
        elif isinstance(value, str) and key not in ['单价', '金额', '税额', '数量', '总金额', '总税额', '发票代码', '发票号码', 
                                                  '项目_单价', '项目_金额', '项目_税额', '项目_数量']:
            # 不处理数字相关字段，只处理文本内容
            result[key] = clean_text_content(value)
    
    # 特别处理商品列表中的商品名称
    for item_key in ['项目列表', '商品列表', '货物或应税劳务、服务名称', '商品或服务清单']:
        if item_key in result and isinstance(result[item_key], list):
            for i, item in enumerate(result[item_key]):
                if isinstance(item, dict):
                    for name_key in ['项目_名称', '名称', '商品名称', '货物或应税劳务、服务名称']:
                        if name_key in item and isinstance(item[name_key], str):
                            item[name_key] = clean_text_content(item[name_key])
                            
                            # 特别处理 - 如果名称是模糊识别且看起来一样，考虑统一处理
                            if i > 0 and len(result[item_key]) > 1:
                                # 检查相邻项是否可能为同一商品类型
                                prev_items = result[item_key][:i]
                                for prev_item in prev_items:
                                    # 查找可能存在的名称键
                                    prev_name_key = None
                                    for pkey in ['项目_名称', '名称', '商品名称', '货物或应税劳务、服务名称']:
                                        if pkey in prev_item and prev_item[pkey] != '-':
                                            prev_name_key = pkey
                                            break
                                    
                                    if prev_name_key and prev_name_key in prev_item:
                                        # 如果两个名称长得类似，或者当前名称是可疑的
                                        current_name = item[name_key]
                                        prev_name = prev_item[prev_name_key]
                                        
                                        # 如果名称看起来很可疑或类似于常见乱编，且之前有更合理的名称
                                        suspicious_names = ['拉', '视', '机', '那']
                                        if any(s in current_name for s in suspicious_names) or len(current_name) <= 2:
                                            # 看是否前面有更合理的商品名称可以替代
                                            if len(prev_name) > 2 and prev_name != '-':
                                                item[name_key] = prev_name
                                                break
    
    # 确保JSON结构的完整性
    result = validate_and_fix_result(result)
    
    return result

def process_invoice(image_path, save_path=None, raw_output=False, preserve_numbers=True, retries=3):
    """处理发票图片并返回结构化信息
    
    Args:
        image_path: 发票图片的路径
        save_path: 保存结果的路径，如果为None则不保存
        raw_output: 是否返回原始输出
        preserve_numbers: 是否保持原始数字格式
        retries: 重试次数，默认为3次
        
    Returns:
        dict: 结构化的发票信息
    """
    # 检查图片路径是否存在
    if not os.path.exists(image_path):
        print(f"错误: 无法找到图片文件 {image_path}")
        return None
    
    # 读取和编码图片（仅用于检查文件可读）
    try:
        with open(image_path, "rb") as image_file:
            image_data = image_file.read()
    except Exception as e:
        print(f"读取图片时出错: {e}")
        return None
    
    # 构建提示词
    prompt = build_prompt(preserve_numbers)
    
    console.print(f"[bold]正在处理发票图像: {image_path}[/bold]")
    console.print("[bold blue]正在识别发票内容...[/bold blue]")
    
    last_error = None
    all_results = []  # 存储所有有效结果
    best_result = None
    
    for attempt in range(retries):
        try:
            # 针对不同重试次数使用不同的策略
            if attempt > 0:
                console.print(f"[yellow]重试第 {attempt+1} 次...[/yellow]")
                
                # 第二次尝试 - 使用不同的提示词
                if attempt == 1:
                    retry_prompt = f"{prompt}\n\n这是重试。请特别仔细识别所有内容，从上到下识别所有商品项目，直到没有更多商品。保持数字的原始格式，不进行舍入或修改。"
                # 第三次尝试 - 拆分任务，先识别基本信息，再专注于商品项
                elif attempt == 2:
                    retry_prompt = "请先提取该发票的基本信息（发票代码、发票号码、开票日期、购买方和销售方信息），然后再特别关注商品项目清单。提取所有可见的商品项目，从上到下完整扫描整个表格区域。对于无法确定的内容使用'-'表示，而不要猜测。请以JSON格式返回结果。"
                # 更多重试 - 进一步简化要求，只关注关键信息
                else:
                    retry_prompt = "请提取这张发票的关键信息，包括发票代码、发票号码、开票日期、购买方名称、销售方名称，以及最重要的，所有商品项目信息（包括名称、规格型号、单位、数量、单价、金额）。以JSON格式返回。"
                
                # 如果有之前的结果，检查数字格式问题
                if best_result:
                    number_issues = []
                    for item_key in ['项目列表', '商品列表', '货物或应税劳务、服务名称', '商品或服务清单']:
                        if item_key in best_result and isinstance(best_result[item_key], list):
                            for item in best_result[item_key]:
                                if isinstance(item, dict):
                                    for price_field in ['项目_单价', '单价']:
                                        if price_field in item and item[price_field].isdigit() and len(item[price_field]) > 2:
                                            number_issues.append(f"{price_field}可能缺少小数点")
                                    
                                    for amount_field in ['项目_金额', '金额']:
                                        if amount_field in item and item[amount_field].isdigit() and len(item[amount_field]) > 2:
                                            number_issues.append(f"{amount_field}可能缺少小数点")
                    
                    # 如果发现数字问题，添加到提示中
                    if number_issues:
                        unique_issues = list(set(number_issues))
                        retry_prompt += "\n\n特别注意: " + ", ".join(unique_issues) + "。请确保数字格式正确，保留原始小数位。"
                
                # 每次重试时尝试调整图像处理方法
                if attempt == 1:
                    # 第二次尝试，直接使用原始图像
                    response = call_api(retry_prompt, image_path)
                elif attempt == 2:
                    # 第三次尝试，尝试压缩图像
                    try:
                        compressed_path = compress_image(image_path)
                        if compressed_path:
                            console.print(f"[yellow]使用压缩图像重试: {compressed_path}[/yellow]")
                            response = call_api(retry_prompt, compressed_path)
                        else:
                            response = call_api(retry_prompt, image_path)
                    except Exception:
                        response = call_api(retry_prompt, image_path)
                else:
                    # 更多重试，尝试其他方法
                    try:
                        # 尝试使用base64编码
                        base64_image = encode_image_to_base64(image_path)
                        response = call_api(retry_prompt, base64_image)
                    except Exception:
                        response = call_api(retry_prompt, image_path)
            else:
                # 第一次尝试，使用原始提示词
                first_prompt = f"{prompt}\n\n请特别注意数字的小数点，特别是单价字段。单价通常是带小数的数值，如8.849558要保持原样，17.52而非1752。请保留所有小数位，不要四舍五入。确保从上到下识别所有商品项目，直到没有更多商品。同时确保生成的JSON结构完整且有效。"
                response = call_api(first_prompt, image_path)
            
            # 处理API响应
            if response:
                result = extract_json_from_response(response)
                
                # 如果提取失败但还有重试机会
                if not result and attempt < retries - 1:
                    console.print("[bold red]无法提取JSON结果，将重试...[/bold red]")
                    continue
                
                # 如果有结果，进行处理
                if result:
                    # 清理可能的乱编内容
                    result = clean_result(result)
                    
                    # 修正数字格式问题，特别是小数点
                    result = fix_number_formats(result)
                    
                    # 确保数值格式保持原样
                    result = preserve_number_formats(result)
                    
                    # 确保结果结构的完整性
                    result = validate_and_fix_result(result)
                    
                    # 添加到有效结果列表中
                    all_results.append(result)
                    
                    # 更新最佳结果
                    if not best_result or is_better_result(result, best_result):
                        best_result = result
                
                # 如果是最后一次尝试或已有很好的结果，就终止
                if attempt == retries - 1 or (best_result and is_good_enough(best_result)):
                    if best_result:
                        console.print("[bold green]识别完成！[/bold green]")
                        
                        # 最后一次清理可能的乱编内容
                        best_result = clean_result(best_result)
                        
                        # 保存结果到文件
                        if save_path:
                            try:
                                with open(save_path, 'w', encoding='utf-8') as f:
                                    json.dump(best_result, f, ensure_ascii=False, indent=2)
                                console.print(f"[bold green]结果已保存到: {save_path}[/bold green]")
                            except Exception as e:
                                console.print(f"[bold red]保存结果时出错: {str(e)}[/bold red]")
                        
                        if raw_output and response:
                            return response
                        
                        # 打印结果
                        formatted_json = json.dumps(best_result, ensure_ascii=False, indent=2)
                        console.print("\n[bold]提取结果:[/bold]")
                        console.print(formatted_json)
                        
                        return best_result
                    
            elif attempt < retries - 1:
                # 如果没有响应但还有重试机会
                console.print("[bold yellow]未获取有效响应，将重试...[/bold yellow]")
                continue
                
        except Exception as e:
            last_error = e
            console.print(f"[bold red]处理发票时出错: {str(e)}[/bold red]")
            if debug_mode:
                import traceback
                traceback.print_exc()
            
            if attempt < retries - 1:
                # 还有重试机会
                continue
    
    # 所有尝试结束后，返回最佳结果（如果有）
    if best_result:
        console.print("[bold yellow]识别可能不完整，返回最佳结果[/bold yellow]")
        
        # 最后一次清理可能的乱编内容
        best_result = clean_result(best_result)
        
        # 保存结果到文件
        if save_path:
            try:
                with open(save_path, 'w', encoding='utf-8') as f:
                    json.dump(best_result, f, ensure_ascii=False, indent=2)
                console.print(f"[bold green]结果已保存到: {save_path}[/bold green]")
            except Exception as e:
                console.print(f"[bold red]保存结果时出错: {str(e)}[/bold red]")
        
        return best_result
    
    # 所有重试都失败且没有任何有效结果，尝试本地OCR作为最后手段
    try:
        console.print("[bold yellow]所有API尝试都失败，尝试使用本地OCR进行基础识别...[/bold yellow]")
        local_result = try_local_ocr(image_path)
        if local_result:
            console.print("[bold green]使用本地OCR成功提取部分信息[/bold green]")
            
            # 保存结果到文件
            if save_path:
                with open(save_path, 'w', encoding='utf-8') as f:
                    json.dump(local_result, f, ensure_ascii=False, indent=2)
                console.print(f"[bold green]本地OCR结果已保存到: {save_path}[/bold green]")
            
            return local_result
    except Exception as e:
        console.print(f"[bold red]本地OCR也失败: {str(e)}[/bold red]")
    
    # 所有重试都失败且没有任何有效结果
    if last_error:
        console.print(f"[bold red]所有重试都失败，最后错误: {str(last_error)}[/bold red]")
    
    return None

def try_local_ocr(image_path):
    """尝试使用本地OCR库进行基本文字识别"""
    try:
        import pytesseract
        from PIL import Image
        
        console.print("[yellow]正在使用pytesseract进行本地OCR...[/yellow]")
        
        # 打开图像
        img = Image.open(image_path)
        
        # 使用pytesseract进行OCR
        text = pytesseract.image_to_string(img, lang='chi_sim')
        
        # 如果没有识别到文字，返回None
        if not text or len(text.strip()) < 10:
            return None
        
        # 尝试从文本中提取基本信息
        result = {
            "注意": "此结果由本地OCR生成，准确性有限",
            "原始文本": text
        }
        
        # 尝试提取发票代码
        import re
        
        # 发票代码
        code_match = re.search(r'发票代码[：:]\s*(\d+)', text)
        if code_match:
            result['发票代码'] = code_match.group(1)
        
        # 发票号码
        number_match = re.search(r'发票号码[：:]\s*(\d+)', text)
        if number_match:
            result['发票号码'] = number_match.group(1)
        
        # 开票日期
        date_match = re.search(r'(\d{4}年\d{1,2}月\d{1,2}日)', text)
        if date_match:
            result['开票日期'] = date_match.group(1)
        
        # 尝试提取商品信息 - 这是一个简化的尝试，可能不准确
        items = []
        lines = text.split('\n')
        for line in lines:
            # 寻找可能的商品行 - 包含数字和单位的行
            if re.search(r'\d+\.?\d*\s*[个件套吨千克箱瓶罐]', line):
                items.append({"商品信息": line.strip()})
        
        if items:
            result['商品列表'] = items
        
        return result
    
    except ImportError:
        console.print("[yellow]没有安装pytesseract库，无法使用本地OCR[/yellow]")
        return None
    except Exception as e:
        console.print(f"[red]本地OCR失败: {e}[/red]")
        return None

def is_better_result(new_result, current_best):
    """判断新结果是否比当前最佳结果更好"""
    if not isinstance(new_result, dict) or not isinstance(current_best, dict):
        return False
    
    # 评分项：完整性、数字格式正确性、乱编内容少
    new_score = 0
    best_score = 0
    
    # 检查关键字段
    key_fields = ['发票代码', '发票号码', '开票日期', '购买方名称', '销售方名称']
    for field in key_fields:
        if field in new_result and new_result[field] and new_result[field] != "-":
            new_score += 1
        if field in current_best and current_best[field] and current_best[field] != "-":
            best_score += 1
    
    # 检查商品列表完整性 - 这里特别增加了对商品项数量的重视
    for item_key in ['项目列表', '商品列表', '货物或应税劳务、服务名称', '商品或服务清单']:
        if item_key in new_result and isinstance(new_result[item_key], list):
            # 每个商品项基础分2分
            item_count = len(new_result[item_key])
            new_score += item_count * 3  # 增加了商品项的权重
            
            # 检查商品名称是否包含明显的乱编，如果有则扣分
            suspicious_names = ['拉完', '视亲', '机算', '那个']
            for item in new_result[item_key]:
                if isinstance(item, dict):
                    # 检查各种可能的名称字段
                    name = None
                    for name_field in ['项目_名称', '名称', '商品名称']:
                        if name_field in item:
                            name = item[name_field]
                            break
                            
                    if name and any(susp in name for susp in suspicious_names):
                        new_score -= 3  # 每个可疑名称扣3分
        
        if item_key in current_best and isinstance(current_best[item_key], list):
            # 同样检查当前最佳结果
            item_count = len(current_best[item_key])
            best_score += item_count * 3  # 增加了商品项的权重
            
            suspicious_names = ['拉完', '视亲', '机算', '那个']
            for item in current_best[item_key]:
                if isinstance(item, dict):
                    # 检查各种可能的名称字段
                    name = None
                    for name_field in ['项目_名称', '名称', '商品名称']:
                        if name_field in item:
                            name = item[name_field]
                            break
                            
                    if name and any(susp in name for susp in suspicious_names):
                        best_score -= 3  # 每个可疑名称扣3分
    
    # 检查数字格式正确性
    for item_key in ['项目列表', '商品列表', '货物或应税劳务、服务名称', '商品或服务清单']:
        if item_key in new_result and isinstance(new_result[item_key], list):
            for item in new_result[item_key]:
                if isinstance(item, dict):
                    # 检查可能的单价字段
                    for price_field in ['项目_单价', '单价']:
                        if price_field in item:
                            # 如果单价有小数点且格式正确
                            if '.' in item[price_field]:
                                new_score += 2
                            # 否则可能有问题
                            else:
                                new_score -= 1
                            break
        
        if item_key in current_best and isinstance(current_best[item_key], list):
            for item in current_best[item_key]:
                if isinstance(item, dict):
                    # 检查可能的单价字段
                    for price_field in ['项目_单价', '单价']:
                        if price_field in item:
                            # 如果单价有小数点且格式正确
                            if '.' in item[price_field]:
                                best_score += 2
                            # 否则可能有问题
                            else:
                                best_score -= 1
                            break
    
    # 检查购买方和销售方名称是否有明显乱编
    suspicious_company_names = ['经细人', '都束花花', '还该', '微信用户']
    for field in ['购买方名称', '销售方名称']:
        if field in new_result and new_result[field]:
            if any(susp in new_result[field] for susp in suspicious_company_names):
                new_score -= 5  # 公司名包含可疑内容，大幅度扣分
        
        if field in current_best and current_best[field]:
            if any(susp in current_best[field] for susp in suspicious_company_names):
                best_score -= 5
    
    return new_score > best_score

def is_good_enough(result):
    """判断结果是否足够好，可以提前结束识别"""
    if not isinstance(result, dict):
        return False
    
    # 检查关键字段
    key_fields = ['发票代码', '发票号码', '开票日期', '购买方名称', '销售方名称']
    key_fields_present = sum(1 for field in key_fields if field in result and result[field])
    
    # 我们不再依赖于预期项目数量，而是专注于质量和完整性
    has_good_items = False
    for item_key in ['项目列表', '商品列表', '货物或应税劳务、服务名称', '商品或服务清单']:
        if item_key in result and isinstance(result[item_key], list):
            item_count = len(result[item_key])
            # 检查单价格式是否正确
            price_format_correct = True
            for item in result[item_key]:
                if isinstance(item, dict):
                    # 检查可能的单价字段
                    price_found = False
                    for price_field in ['项目_单价', '单价']:
                        if price_field in item:
                            # 单价应该有小数点
                            if not ('.' in item[price_field]):
                                price_format_correct = False
                            price_found = True
                            break
                    
                    # 如果没有找到任何单价字段，也视为格式不正确
                    if not price_found:
                        price_format_correct = False
                        break
            
            # 如果有商品项且价格格式正确，我们认为商品列表足够好
            if item_count > 0 and price_format_correct:
                has_good_items = True
                break
    
    # 主要判断标准：关键字段存在且商品列表质量好
    return key_fields_present >= 3 and has_good_items

# 检查人工识别表格的商品项数，用于后续验证
def count_items_in_image(image_path):
    """尝试通过图像分析估计发票上的商品项数量"""
    try:
        # 这里只是一个启发式估计，实际运行中可能需要更复杂的图像分析
        from PIL import Image
        import numpy as np
        
        # 打开图像
        img = Image.open(image_path)
        
        # 转换为灰度图
        img_gray = img.convert('L')
        
        # 转换为numpy数组
        img_data = np.array(img_gray)
        
        # 简单的行检测（只是一个启发式方法）
        # 检测图像中可能的水平线条，作为表格行的指示
        horizontal_profile = np.sum(img_data < 128, axis=1)  # 沿水平方向求和，检测暗像素
        
        # 尝试识别表格区域中的行数
        from scipy import signal
        
        # 使用峰值检测查找可能的表格行
        peaks, _ = signal.find_peaks(horizontal_profile, distance=20)  # 假设行之间至少有20像素
        
        # 这只是一个启发式估计，返回可能的行数范围
        min_items = max(3, len(peaks) // 2)  # 至少3行，否则取检测到的峰值数一半
        max_items = max(6, len(peaks))       # 至少6行，否则取检测到的峰值数
        
        return (min_items, max_items)
    except Exception as e:
        if debug_mode:
            print(f"尝试分析图像中的商品项数失败: {e}")
        return (3, 10)  # 默认估计范围

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='发票OCR处理工具')
    parser.add_argument('image', type=str, help='发票图像文件路径')
    parser.add_argument('--output', '-o', type=str, help='输出JSON文件路径')
    parser.add_argument('--raw', action='store_true', help='输出原始响应')
    parser.add_argument('--debug', action='store_true', help='启用调试模式')
    parser.add_argument('--retries', '-r', type=int, default=3, help='识别失败时的重试次数')
    parser.add_argument('--retry-delay', type=int, default=1, help='重试之间的延迟时间(秒)')
    parser.add_argument('--local', action='store_true', help='如果API失败，使用本地OCR')
    
    args = parser.parse_args()
    
    # 设置调试模式
    if args.debug:
        debug_mode = True
        print("调试模式已启用")
    
    # 处理发票
    result = process_invoice(
        image_path=args.image, 
        save_path=args.output, 
        raw_output=args.raw,
        retries=args.retries
    )
    
    # 如果没有指定输出文件，则在控制台打印结果
    if result and not args.output:
        if isinstance(result, dict):
            print(json.dumps(result, ensure_ascii=False, indent=2))
        else:
            print(result)